2012-09-12 00:23:00
在前面几篇文章里,我们已经成功脱离ATL写了一个COM组件,并且实现了自动化。今天,我们来加入第二个类,并且为加入第二个类做一些整理工作。
@ 为DLL建立一个Module类
在前面,我们为了使得DllCanUnloadNow能正确工作而放了一个全局变量LONG g_nModuleCount,并且在SampleClass的构造函数和析构函数里对它进行自增和自减。另外还有个ITypeLib,也是全局的。为了将这些零散的东西收集在一起,我们建立一个ComModule类,地位类似MFC的CWinApp,作为这个DLL中的唯一的全局对象。
1class ComModule
2{
3public:
4 ComModule(HMODULE hModule = nullptr) :
5 m_hModule(hModule), m_nGlobalRefCount(0), m_pTypeLib(nullptr)
6 {
7 TCHAR szModulePath[MAX_PATH] = {};
8 GetModuleFileName(m_hModule, szModulePath, ARRAYSIZE(szModulePath));
9
10 m_strModulePath = szModulePath;
11
12 LoadTypeLib(szModulePath, &m_pTypeLib);
13 }
14
15 ~ComModule()
16 {
17 if (m_pTypeLib != nullptr)
18 {
19 m_pTypeLib->Release();
20 m_pTypeLib = nullptr;
21 }
22 }
23
24public:
25 ULONG GlobalAddRef()
26 {
27 return (ULONG)InterlockedIncrement(&m_nGlobalRefCount);
28 }
29
30 ULONG GlobalRelease()
31 {
32 return (ULONG)InterlockedDecrement(&m_nGlobalRefCount);
33 }
34
35private:
36 HMODULE m_hModule;
37 String m_strModulePath;
38 LONG m_nGlobalRefCount;
39 ITypeLib *m_pTypeLib;
40};
然后,定义一个全局指针g_pComModule:
1__declspec(selectany) ComModule *g_pComModule = nullptr;
要求在DllMain里面new/delete一个ComModule:
1BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
2{
3 switch (ul_reason_for_call)
4 {
5 case DLL_PROCESS_ATTACH:
6 g_pComModule = new ComModule(hModule);
7 break;
8 case DLL_THREAD_ATTACH:
9 break;
10 case DLL_THREAD_DETACH:
11 break;
12 case DLL_PROCESS_DETACH:
13 delete g_pComModule;
14 break;
15 default:
16 break;
17 }
18
19 return TRUE;
20}
再把全局对象计数放到所有COM类的公共基类ComClass里。
1template <typename T>
2class ComClass
3{
4public:
5 ComClass()
6 {
7 if (g_pComModule != nullptr)
8 {
9 g_pComModule->GlobalAddRef();
10 }
11 }
12
13 ~ComClass()
14 {
15 if (g_pComModule != nullptr)
16 {
17 g_pComModule->GlobalRelease();
18 }
19 }
20
21 // ...
22};
这样,每个COM对象会自动向Module类报告引用计数,这边解决了DllCanUnloadNow的问题。
同时,为了方便建立一个新的DLL,我们把需要导出的那几个函数也都做在ComModule里面,使得DLL那边只需要写下面这些就够了:
1STDAPI DllCanUnloadNow()
2{
3 return xl::g_pComModule->DllCanUnloadNow();
4}
5
6STDAPI DllGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Outptr_ LPVOID *ppv)
7{
8 return xl::g_pComModule->DllGetClassObject(rclsid, riid, ppv);
9}
10
11STDAPI DllRegisterServer()
12{
13 return xl::g_pComModule->DllRegisterServer();
14}
15
16STDAPI DllUnregisterServer()
17{
18 return xl::g_pComModule->DllUnregisterServer();
19}
20
21STDAPI DllInstall(BOOL bInstall, _In_opt_ LPCTSTR lpszCmdLine)
22{
23 return xl::g_pComModule->DllInstall(bInstall, lpszCmdLine);
24}
25
26ComModule::DllCanUnloadNow的实现代码为:
27
28HRESULT DllCanUnloadNow()
29{
30 return m_nGlobalRefCount > 0 ? S_FALSE : S_OK;
31}
现在需要解决的是ComModule::DllGetClassObject。在之前的代码里,我们直接写死一个ClassFactory,ClassFactory里面对应写死的SampleClass。现在要假设有好多COM类的情形。
类厂我们可以搞个模版,ClassFactory,T就是对应的COM类,在类厂里面 new T 就可以了。问题是DllGetClassObject只有CLSID,也就是__uuidof(T),没有T本身。从__uuidof(T)得到T,是件不容易的事情,__uuidof(T)是数据不是类型,没法采用类似萃取的方法。
我们的名言是,不会做了,于就抄ATL的……
不知大家有木有发现,ATL生成的COM类的头文件的最后有一句这样子的话:
1OBJECT_ENTRY_AUTO(__uuidof(CTheClass), TheClass)
(惭愧的是,我曾经有一次把它当成垃圾代码删除掉了,导致对象怎么也创建不出来,还为此调试了好久……)
这个宏将分散在各个头文件的类定义搜集在一起,形成内存连续的全局常量,以便我们在DllGetClassObject之中查表。具体的手法可以参考下面两篇文章: 《巧妙的Section — — 剖析ATL OBJECT_MAP的自动建立》 《The Object Map》
它的手法是挺巧妙,可是太编译器相关了,相关得我不想抄……可是又暂时想不出其他办法,只好抄了……
由于数据里无法存储“类型”,只能存储数据,于是我们为Factory类定义Factory的Factory:
1template <typename T>
2class ClassFactory : public ComClass<ClassFactory<T>>,
3 public IClassFactoryImpl<>
4{
5public:
6 static IClassFactory *CreateFactory()
7 {
8 return new ClassFactory;
9 }
10
11 // ...
12};
这样,对于每个COM类T,我们只需要存储__uuidof(T)以及ClassFactory::CreateFactory,便可以在DllGetClassObject中查表,由CLSID查到类厂创建函数,从而得到类厂实例。
表结构定义:
1typedef IClassFactory *(*ClassFactoryCreator)();
2
3struct ClassEntry
4{
5 const CLSID *pClsid;
6 ClassFactoryCreator pfnCreator;
7};
然后抄ATL的手法:
1#pragma section("XL_COM$__a", read)
2#pragma section("XL_COM$__m", read)
3#pragma section("XL_COM$__z", read)
4
5extern "C"
6{
7 __declspec(selectany) __declspec(allocate("XL_COM$__a"))
8 const ClassEntry *LP_CLASS_BEGIN = nullptr;
9 __declspec(selectany) __declspec(allocate("XL_COM$__z"))
10 const ClassEntry *LP_CLASS_END = nullptr;
11}
12
13#if !defined(_M_IA64)
14#pragma comment(linker, "/merge:XL_COM=.rdata")
15#endif
16
17#if defined(_M_IX86)
18#define XL_CLASS_MAP_PRAGMA(class) __pragma(comment(linker, "/include:_LP_CLASS_ENTRY_" # class));
19#elif defined(_M_IA64) || defined(_M_AMD64)
20#define XL_CLASS_MAP_PRAGMA(class) __pragma(comment(linker, "/include:LP_CLASS_ENTRY_" # class));
21#else
22#error Unknown Platform. define XL_CLASS_MAP_PRAGMA
23#endif
24
25#define XL_DECLARE_COM_CLASS(class) \
26 \
27 const ClassEntry CLASS_ENTRY_##class = \
28 { \
29 &__uuidof(class), \
30 &ClassFactory<class>::CreateFactory \
31 }; \
32 extern "C" __declspec(allocate("XL_COM$__m")) __declspec(selectany) \
33 const ClassEntry * LP_CLASS_ENTRY_##class = &CLASS_ENTRY_##class; \
34 XL_CLASS_MAP_PRAGMA(class) \
呃……虽然上面给出了两篇文章,还是忍不住亲自讲一下。首先是:
1#pragma section("XL_COM$__a", read)
2#pragma section("XL_COM$__m", read)
3#pragma section("XL_COM$__z", read)
看上去是定义了三个段,实际上最后出现在PE文件里的只有一个段。如果没有后面的merge,用工具查看编译出的文件结构,结果是这样的:

新增的自定义的段为“XL_COM”。
实际上编译器在处理段名的时候,只读取到$符号前面的字符,后面的附加后缀只存在于编译期间。目前知道的用途,便是排序,__a会在__m之前,__m会在__z之前。
一开始我们就在 XL_COM$__a和XL_COM$__z分别保存了一个空指针,之后每次调用宏XL_DECLARE_COM_CLASS,就在XL_COM$__m放一个指向真实ClassEntry的指针。
最后的一句,#pragma comment(linker, "/merge:XL_COM=.rdata")用于把段XL_COM合并到.rdata,并保持原来在XL_COM里头的数据不变。
经过以上一系列处理,我们便将一张COM类表保存在了全局数据中。
1__pragma(comment(linker, "/include: ..."))的作用不知道,MSDN解释看不懂,有人知道吗?
最后,ComModule::DllGetClassEntry就可以这样写了:
1HRESULT DllGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Outptr_ LPVOID *ppv)
2{
3 for (const ClassEntry * const *ppEntry = &LP_CLASS_BEGIN + 1; ppEntry < &LP_CLASS_END; ++ppEntry)
4 {
5 if (*ppEntry == nullptr)
6 {
7 continue;
8 }
9
10 if (rclsid == *(*ppEntry)->pClsid)
11 {
12 IClassFactory *pClassFactory = (*ppEntry)->pfnCreator();
13 return pClassFactory->QueryInterface(riid, ppv);
14 }
15 }
16
17 return CLASS_E_CLASSNOTAVAILABLE;
18}
之所以要判断*ppEntry是否为空,是因为Debug编译的时候,各个指针之间会被插入大量的零数据……ATL也是这么搞的。
这样,我们在每个COM类声明后,也可以像ATL的一样,写一句
1XL_DECLARE_COM_CLASS(SampleClass);
就可以让DllGetClassObject找到对应的类厂了。
只是不知道ATL的OBJECT_ENTRY_AUTO(__uuidof(CTheClass), TheClass)为什么需要我们提供两个参数,第一个参数是CLSID,明显它可以根据第二个参数自己去拿到的嘛……
注册和反注册部分,我们之前也是写死的,现在要换成活的。
我一开始的方案是,从ComModule里的ITypeLib出发,找到每一个coclass,然后注册CLSID,以及注册TypeLib。但是问题是ProgID,原始IDL文件里面并没有ProgID,因此ITypeLib里面也不可能拿到。我觉得ProgID不能牺牲,所以这个方案不行。
ATL的方案是使用RGS文件,然后注册的时候解析RGS文件并写注册表。我比较讨厌RGS文件……原因是文件格式找不到官方说明,很坑爹的是,字符串值的写法“s ‘SomeString’”的“s”和单引号之间必须有个空格!多少个漆黑的夜里,把rgs改了一下,就再也无法正确注册,然后把原始的拷过来,小心翼翼的一个一个修改、检验,才能发现问题所在……
我决定借用上一节的表,把ClassEntry改为:
1struct ClassEntry
2{
3 const CLSID *pClsid;
4 ClassFactoryCreator pfnCreator;
5 LPCTSTR lpszClassDesc; // SampleClass Class
6 LPCTSTR lpszProgID; // COMProvider.SampleClass
7 LPCTSTR lpszVersion; // 1
8};
其中的lpszProgID为VersionIndependentProgID,带Version的ProgID使用最后一个lpVersion拼到lpszProgID的后面。
这样,注册Class的函数便可写成类似下面这样子:
1bool RegisterComClasses(HKEY hRootKey)
2{
3 for (const ClassEntry * const *ppEntry = &LP_CLASS_BEGIN + 1; ppEntry < &LP_CLASS_END; ++ppEntry)
4 {
5 if (*ppEntry == nullptr)
6 {
7 continue;
8 }
9
10 // ...
11 }
12
13 return true;
14}
同时可以定义其他三个函数:
代码不贴了。其中TypeLib的信息从ITypeLib中读取。
最后,对外开放的那个两个函数可以简单地实现为:
1HRESULT DllRegisterServer()
2{
3 if (!RegisterTypeLib(HKEY_LOCAL_MACHINE))
4 {
5 return E_FAIL;
6 }
7
8 if (!RegisterComClasses(HKEY_LOCAL_MACHINE))
9 {
10 return E_FAIL;
11 }
12
13 return S_OK;
14}
15
16HRESULT DllUnregisterServer()
17{
18 if (!UnregisterComClasses(HKEY_LOCAL_MACHINE))
19 {
20 return E_FAIL;
21 }
22
23 if (!UnregisterTypeLib(HKEY_LOCAL_MACHINE))
24 {
25 return E_FAIL;
26 }
27
28 return S_OK;
29}
这里只管CLSID、ProgID、TypeLib,AppID啥的不管了。
现在我们来面对之前一直置之不理的DllInstall。函数原型为:
1STDAPI DllInstall(BOOL bInstall, _In_opt_ LPCWSTR pszCmdLine)
MSDN文档页面: http://msdn.microsoft.com/en-us/library/windows/desktop/bb759846.aspx
第一个参数,bInstall,当我们在调用regsvr32的时候指定参数/u的时候,它是FALSE,不指定/u的时候,它是TRUE。
第二个参数,当我们在调用regsvr32指定/i:XXX的时候,pszCmdLine为字符串XXX。当直接使用/i或者/i:的时候,pszCmdLine为空字符串。
只有使用了/i参数,regsvr32才会调用DllInstall。
如果指定了/i,且未指定/n,注册的时候regsvr32会先调用DllRegisterServer,后调用DllInstall(TRUE, …),反注册的时候regsvr32会先调用DllInstall(FALSE, …),后调用DllRegisterServer。 如果指定了/i且指定了/n,regsvr32仅仅调用DllInstall。
好了,文档解释到这里。ATL默认代码为:
1STDAPI DllInstall(BOOL bInstall, _In_opt_ LPCWSTR pszCmdLine)
2{
3 HRESULT hr = E_FAIL;
4 static const wchar_t szUserSwitch[] = L"user";
5
6 if (pszCmdLine != NULL)
7 {
8 if (_wcsnicmp(pszCmdLine, szUserSwitch, _countof(szUserSwitch)) == 0)
9 {
10 ATL::AtlSetPerUserRegistration(true);
11 }
12 }
13
14 if (bInstall)
15 {
16 hr = DllRegisterServer();
17 if (FAILED(hr))
18 {
19 DllUnregisterServer();
20 }
21 }
22 else
23 {
24 hr = DllUnregisterServer();
25 }
26
27 return hr;
28}
其中的“ATL::AtlSetPerUserRegistration(true)”据说会把DllRegisterServer和DllUnregisterServer的注册表位置由HKLM重定向到HKCU。也就是说,当使用regsvr32 /i:user的时候,支持注册到当前用户。
既然ATL默认生成的COM DLL支持/i:user参数,我们也要模拟一下,装得正式一点:
1HRESULT DllInstall(BOOL bInstall, _In_opt_ LPCTSTR lpszCmdLine)
2{
3 if (lpszCmdLine == nullptr)
4 {
5 return E_INVALIDARG;
6 }
7
8 if (_tcsicmp(lpszCmdLine, _T("User")) == 0)
9 {
10 if (bInstall)
11 {
12 if (!RegisterTypeLib(HKEY_CURRENT_USER))
13 {
14 return E_FAIL;
15 }
16
17 if (!RegisterComClasses(HKEY_CURRENT_USER))
18 {
19 return E_FAIL;
20 }
21
22 return S_OK;
23 }
24 else
25 {
26 if (!UnregisterComClasses(HKEY_CURRENT_USER))
27 {
28 return E_FAIL;
29 }
30
31 if (!UnregisterTypeLib(HKEY_CURRENT_USER))
32 {
33 return E_FAIL;
34 }
35
36 return S_OK;
37 }
38 }
39
40 return E_FAIL;
41}
好了,到目前为止我们已经支持了所有标准的注册方式了。除了/i:user,其实我们可以在这里做一些扩展,支持一些自定义的注册方式,来脱离注册表依赖,此为后话。
准备工作做完了,现在着手加入第二个COM类。
且慢,还有一点点荣誉。在上一篇,为了实现自动化,我们在对象类加入了IDispatch的实现代码。但这些代码是机械的、可抄的,因此可以写成一个独立的东西,IDispatchImpl已经被占用了,就叫Dispatcher吧。
现在SampleClass干净了,代码清单为: SampleClass.h
1#include "COMProvider_h.h"
2#include <xl/Win32/COM/xlDispatcher.h>
3
4class SampleClass : public xl::ComClass<SampleClass>,
5 public xl::Dispatcher<ISampleInterface>
6{
7public:
8 STDMETHOD(SampleMethod)();
9
10public:
11 XL_COM_INTERFACE_BEGIN(SampleClass)
12 XL_COM_INTERFACE(ISampleInterface)
13 XL_COM_INTERFACE(IDispatch)
14 XL_COM_INTERFACE_END()
15};
16
17XL_DECLARE_COM_CLASS(SampleClass,
18 _T("Streamlet COMProvider Sample Class"),
19 _T("Streamlet.COMProvider.SampleClass"),
20 _T("1"));
21
22SampleClass.cpp
23#include "SampleClass.h"
24
25STDMETHODIMP SampleClass::SampleMethod()
26{
27 MessageBox(NULL, _T("SampleMethod called."), _T("Info"), MB_OK | MB_ICONINFORMATION);
28 return S_OK;
29}
挺清晰的吧?
第二个COM类的IDL:
import "oaidl.idl";
import "ocidl.idl";
[
object,
uuid(83C783E3-F989-4E0D-BFC5-631273EDFFDA),
]
interface ISampleInterface : IDispatch
{
[id(1)] HRESULT SampleMethod();
};
[
object,
uuid(AD6AD24D-0E31-44A6-A2B3-7B180437541D),
]
interface ISampleInterface2 : IDispatch
{
[id(1)] HRESULT SampleMethod2();
};
[
uuid(22935FC2-282E-4727-B40F-E55128EA1072),
version(1.0),
]
library COMProviderLib
{
importlib("stdole2.tlb");
[
uuid(0DECBFF5-A8A5-49E8-9962-3D18AAC6088E)
]
coclass SampleClass
{
[default] interface ISampleInterface;
};
[
uuid(85431E6A-28C1-483D-A3DE-CEA640899E0E)
]
coclass SampleClass2
{
[default] interface ISampleInterface2;
};
};
import "shobjidl.idl";
注意,我这里加“2”并不表示同一ProgID的升级版,我只是为了和第一个区分。
SampleClass2的实现代码: SampleClass2.h
1#include "COMProvider_h.h"
2#include <xl/Win32/COM/xlDispatcher.h>
3
4class SampleClass2 : public xl::ComClass<SampleClass2>,
5 public xl::Dispatcher<ISampleInterface2>
6{
7public:
8 STDMETHOD(SampleMethod2)();
9
10public:
11 XL_COM_INTERFACE_BEGIN(SampleClass2)
12 XL_COM_INTERFACE(ISampleInterface2)
13 XL_COM_INTERFACE(IDispatch)
14 XL_COM_INTERFACE_END()
15};
16
17XL_DECLARE_COM_CLASS(SampleClass2,
18 _T("Streamlet COMProvider Sample Class 2"),
19 _T("Streamlet.COMProvider.SampleClass2"),
20 _T("1"));
SampleClass2.cpp
1STDMETHODIMP SampleClass2::SampleMethod2()
2{
3 MessageBox(NULL, _T("SampleMethod2 called."), _T("Info"), MB_OK | MB_ICONINFORMATION);
4 return S_OK;
5}
完毕。相关框架代码见: http://xllib.codeplex.com/SourceControl/changeset/view/19794#318174 例子见COMProtocol3.rar(http://pan.baidu.com/s/1hqtJX6c)。
我们走到哪里了? 最近发的比较勤,究竟做了些什么事呢?小小的理一下:
首先,在第一篇《裸写一个含内嵌IE控件的窗口》中,练习了一下如何手工写一个COM类,此时整个模块还不是COM组件。
第二篇《学习下 ATL 的继承链处理》则是个小铺垫,揭示了COM类继承处理上的一个小手法。
第三篇《山寨一下ATL的COM_INTERFACE》是比较全面地学习ATL关于单个COM类的实现上的简化技巧。
第四篇《写个含 Windows Media Player 的窗口》是个整理,验证下前一篇实践代码的合理性和可用性。
第五篇《裸写一个进程内 COM 组件》,是对COM DLL的一个整体实践,不再是单个COM类了。
第六篇《让COM组件可被跨语言调用》是实现自动化,在后面它将作为COM组件的基本功能存在。
第七篇,也就是本文,是对COM DLL的一个整理,把共性整合到框架,简化COM类中的代码,让加入第二个、第三个COM类更方便。
经过这几天的练习,攒下了一批框架性代码,借助于它们,已经可以较为方便地写出一个COM DLL了,就如……使用ATL一样方便。换句话说,我们已经实现了ATL……的一小部分。这是造轮子吗?非也!这是学习过程中的自然积累。
前面的有些做法是抄ATL的,这并不可耻,好方法就拿来用嘛。目前的讨论还仅局限于进程内组件,进程外的并未涉及,COM的加载过程也没有涉及。这些以后再慢慢学。到本文为之,算一个小系列吧,故此总结。
首发:http://www.cppblog.com/Streamlet/archive/2012/09/12/190331.html